﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Common;
using Common.Logging;
using System.Globalization;

namespace WebServer
{
    /// <summary>
    /// A handler which exposes a folder to the caller
    /// </summary>
    public class FilesHandler : BaseRequestHandler
    {
        private static ILog log = LogManager.GetCurrentClassLogger();

        private bool _allowCaching = true;
        /// <summary>
        /// Gets/Sets if the handler will use caching to improve performance (default True)
        /// </summary>
        public bool AllowCaching
        {
            get { return _allowCaching; }
            set { _allowCaching = value; }
        }

        /// <summary>
        /// Constructs a handler that points to the specified directory
        /// </summary>
        /// <param name="root">The directory to expose</param>
        public FilesHandler(DirectoryInfo root, String prefix = null)
        {
            _root = root;
            _prefix = prefix;
            if (!_root.Exists)
            { throw new DirectoryNotFoundException(_root.FullName); }
            init();
        }
        /// <summary>
        /// Constructs a handler that points to the specified directory
        /// </summary>
        /// <param name="root">The directory to expose</param>
        public FilesHandler(String root, String prefix = null)
        {
            _root = new DirectoryInfo(root);
            _prefix = prefix;
            if (!_root.Exists)
            { throw new DirectoryNotFoundException(_root.FullName); }
            init();
        }
        private void init()
        {
            SetupDefaultMimeTypes();
            if (_prefix != null && !_prefix.StartsWith("/"))
            {
                _prefix = "/" + _prefix;
            }
        }

        private String GetMimeType(FileInfo file)
        {
            String ext = file.Extension;
            ext = ext.ToUpperInvariant().Trim();
            String mime = null;
            _mimeTypes.TryGetValue(ext, out mime);
            return mime;
        }

        private DirectoryInfo _root;
        private String _prefix;
        private Dictionary<String, String> _mimeTypes = new Dictionary<string, string>();
        /// <summary>
        /// Adds a mime type to the files handler
        /// </summary>
        /// <param name="extension">The extension to match</param>
        /// <param name="mimeType">The mime type to return for files matching this extension</param>
        public void AddMimeType(String extension, String mimeType)
        {
            _mimeTypes.Add(extension.ToUpperInvariant().Trim(), mimeType.Trim());
        }
        /// <summary>
        /// Removes a mime type from the files handler
        /// </summary>
        /// <param name="extension">The extension to match</param>
        public void RemoveMimeType(String extension)
        {
            _mimeTypes.Remove(extension.ToUpperInvariant().Trim());
        }
        private void SetupDefaultMimeTypes()
        {
            AddMimeType(".gif", "image/gif");
            AddMimeType(".jpg", "image/jpeg");
            AddMimeType(".jpeg", "image/jpeg");
            AddMimeType(".png", "image/png");
            AddMimeType(".js", "application/x-javascript");
            AddMimeType(".htm", "text/html");
            AddMimeType(".html", "text/html");
            AddMimeType(".css", "text/css");
            AddMimeType(".txt", "text/plain");
            AddMimeType(".zip", "application/zip");
        }

        /// <summary>
        /// Gets/Sets if the handler will display a directory listing when a specific file is not specified (Default True)
        /// </summary>
        public bool ShowDirectoryListing
        { get { return _showDirectoryListing; } set { _showDirectoryListing = value; } }
        private bool _showDirectoryListing = true;

        private static readonly String DefaultDirectoryListingTemplate = ResourceUtil.GetResourceAsText("WebServer.Templates.defaultDirectoryListing.template");
        private String _error404Template = ResourceUtil.GetResourceAsText("WebServer.Templates.404FileNotFound.template");

        /// <summary>
        /// The template to use when displaying the directory listing (Default: An embedded resource using english text)
        /// </summary>
        public String DirectoryListingTemplate
        {
            get { return _directoryListingTemplate; }
            set { _directoryListingTemplate = value; }
        }
        private String _directoryListingTemplate = DefaultDirectoryListingTemplate;

        /// <summary>
        /// Handles requests to fetch files from the http server
        /// </summary>
        /// <param name="request">The request</param>
        /// <param name="response">The response</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1309:UseOrdinalStringComparison", MessageId = "System.String.LastIndexOf(System.String,System.StringComparison)")]
        public override void Get(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
        {
            String path = Uri.UnescapeDataString(request.Url.AbsolutePath) ?? "";
            if (_prefix != null && path.StartsWith(_prefix, StringComparison.InvariantCultureIgnoreCase))
            {
                path = "/" + path.Substring(_prefix.Length);
            }
            if (path.Contains("~"))
            {
                path = path.Substring(path.LastIndexOf("~", StringComparison.InvariantCultureIgnoreCase) + 1);
            }
            path = path.Replace("..\\", "").Replace("/","\\").Trim('\\');
            String fullPath = Path.Combine(_root.FullName, path);

            bool isFile = File.Exists(fullPath);
            bool isDir = Directory.Exists(fullPath);


            if (isFile)
            {
                String modifiedSinceString = request.Headers["If-Modified-Since"];
                DateTime modifiedSince = DateTime.MinValue; ;
                if (modifiedSinceString != null)
                {
                    if (!DateTime.TryParse(modifiedSinceString, out modifiedSince))
                    {
                        log.Warn("Could not part date string: " + modifiedSinceString);
                    }
                }
                

                FileInfo file = new FileInfo(fullPath);

                AddCachingHeaders(response, file);

                response.ContentType = GetMimeType(file);
                if (modifiedSince >= file.LastWriteTime)
                {
                    response.StatusCode = 304;
                    log.Trace("304 FILE - " + fullPath);
                }
                else
                {
                    byte[] temp = File.ReadAllBytes(fullPath);
                    response.OutputStream.Write(temp, 0, temp.Length);
                    log.Trace("200 FILE - " + fullPath);
                }
            }
            else if (isDir)
            {
                AddCachingHeaders(response, new DirectoryInfo(fullPath));
                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                { sw.Write(getDirectoryListing(request.Url, new DirectoryInfo(fullPath))); }
                log.Trace("200 DIRECTORY - " + fullPath);
            }
            else
            {
                response.StatusCode = 404;
                using (StreamWriter sw = new StreamWriter(response.OutputStream))
                { sw.Write(get404(request.Url)); }
                log.Trace("404 - " + fullPath);
            }
        }
        private void AddCachingHeaders(System.Net.HttpListenerResponse response, FileSystemInfo info)
        {
            if (!_allowCaching)
            { return; }
            response.Headers["Cache-Control"] = "pubic, max-age=86400";
            response.Headers["Expires"] = DateTime.Now.AddDays(1).ToLongDateTimeString();
            response.Headers["Last-Modified"] = info.LastWriteTime.AddSeconds(1).ToLongDateTimeString();
        }
        

        private String get404(Uri uri)
        {
            String result = String.Empty;
            try { result = _error404Template.Replace("${url}", uri != null ? uri.ToString() : String.Empty); }
            catch { }
            return result;
        }
        private String getDirectoryListing(Uri url, DirectoryInfo dir)
        {            
            String folderRowTemplate = "<tr><td><a href=\"${url}/${Name}\">${Name}</a></td><td></td><td>${LastWriteTime}</td></tr>";
            String fileRowTemplate =   "<tr><td><a href=\"${url}/${Name}\">${Name}</a></td><td>${Length}</td><td>${LastWriteTime}</td></tr>";

            String result = DirectoryListingTemplate;
            result = result.Replace("${url}", url.ToString());

            StringBuilder items = new StringBuilder();
            foreach (DirectoryInfo subdir in dir.GetDirectories())
            {
                String subdirrow = folderRowTemplate;
                subdirrow  = subdirrow .Replace("${url}", url.ToString());
                subdirrow  = subdirrow .Replace("${Name}", subdir.Name);
                subdirrow  = subdirrow .Replace("${LastWriteTime}", subdir.LastWriteTime.ToString());
                items.AppendLine(subdirrow);
            }
            foreach (FileInfo file in dir.GetFiles())
            {
                String filerow = fileRowTemplate;
                filerow = filerow.Replace("${url}", url.ToString());
                filerow = filerow.Replace("${Name}", file.Name);
                filerow = filerow.Replace("${Length}", file.Length.ToString());
                filerow = filerow.Replace("${LastWriteTime}", file.LastWriteTime.ToString());
                items.AppendLine(filerow);
            }
            result = result.Replace("${items}", items.ToString());
            return result;
        }
    }
}
